Python 日志库对比

摘要

文章参考自: 此处,记录了 Python 内置 logger 和 常见的日志库的使用,包括 LoguruStructlog, Eliot, Logbook, Mirosoft 的 Picologging

标准日志模块

1
2
3
4
5
6
7
8
9
10
11
12
13
import logging

logging.debug("A debug message")
logging.info("An info message")
logging.warning("A warning message") # 默认 WARNING 级别,只有 WARNING 以后(包含)的才会输出
logging.error("An error message")
logging.critical("A critical message")

"""
WARNING:root:A warning message
ERROR:root:An error message
CRITICAL:root:A critical message
"""

自定义 Logger

1
2
3
4
import logging

logger = logging.getLogger(__name__)
...

一旦有了自定义的 Logger,就可以使用HandlerFormatterFilter自定义其输出。

以下是使用自定义 Logger 记录到控制台和文件的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import sys
import logging

# 创建 Logger
logger = logging.getLogger("example")
logger.setLevel(logging.DEBUG)

# 创建 Handler,将日志输出到标准输出和文件
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
errHandler = logging.FileHandler("error.log")

# 不同输出源设置不同的日志等级
stdoutHandler.setLevel(logging.DEBUG)
errHandler.setLevel(logging.ERROR)

# 创建日志输出格式
fmt = logging.Formatter("%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s")

# Handler 装配日志格式
stdoutHandler.setFormatter(fmt)
errHandler.setFormatter(fmt)

# Logger 应用 Handler
logger.addHandler(stdoutHandler)
logger.addHandler(errHandler)

logger.info("服务器在端口8080上开始侦听")
logger.warning("驱动器'varlog'上的磁盘空间不足。考虑腾出空间")

try:
raise Exception("连接数据库“my_db”失败")
except Exception as e:
# exc_info=True 包含错误信息
logger.error(e, exc_info=True)

stdout 输出结果

包含 info,warning 等级的日志

1
2
3
4
5
6
7
example: 2023-07-31 13:59:40,921 | INFO | aa.py:27 | 14384 >>> 服务器在端口8080上开始侦听
example: 2023-07-31 13:59:40,921 | WARNING | aa.py:28 | 14384 >>> 驱动器'varlog'上的磁盘空间不足。考虑腾出空间
example: 2023-07-31 13:59:40,921 | ERROR | aa.py:34 | 14384 >>> 连接数据库“my_db”失败
Traceback (most recent call last):
File "D:\project\knowledge-api\aa.py", line 31, in <module>
raise Exception("连接数据库“my_db”失败")
Exception: 连接数据库“my_db”失败

日志文件输出结果

只包含 error 等级的日志

1
2
3
4
5
example: 2023-07-31 13:52:57,416 | ERROR | aa.py:34 | 76 >>> 连接数据库“my_db”失败
Traceback (most recent call last):
File "D:\project\knowledge-api\aa.py", line 31, in <module>
raise Exception("连接数据库“my_db”失败")
Exception: 连接数据库“my_db”失败

结构化输出

logging除非您 实现一些额外的代码,否则该模块无法生成结构化日志。有一种更简单、更好的方法来获取结构化输出: python-json-logger 库:

1
pip install python-json-logger
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import sys
import logging

# 创建 Logger
from pythonjsonlogger import jsonlogger

logger = logging.getLogger("example")
logger.setLevel(logging.DEBUG)

# 创建 Handler,将日志输出到标准输出和文件
stdoutHandler = logging.StreamHandler(stream=sys.stdout)
errHandler = logging.FileHandler("error.log")

# 不同输出源设置不同的日志等级
stdoutHandler.setLevel(logging.DEBUG)
errHandler.setLevel(logging.ERROR)

# 创建日志输出格式
# fmt = logging.Formatter("%(name)s: %(asctime)s | %(levelname)s | %(filename)s:%(lineno)s | %(process)d >>> %(message)s")
fmt = jsonlogger.JsonFormatter(
"%(name)s %(asctime)s %(levelname)s %(filename)s %(lineno)s %(process)d %(message)s",
rename_fields={"levelname": "等级", "asctime": "日期"}, # 重新命令
)

# Handler 装配日志格式
stdoutHandler.setFormatter(fmt)
errHandler.setFormatter(fmt)

# Logger 应用 Handler
logger.addHandler(stdoutHandler)
logger.addHandler(errHandler)

logger.info("服务器在端口8080上开始侦听")
logger.warning("驱动器'varlog'上的磁盘空间不足。考虑腾出空间")

try:
raise Exception("连接数据库“my_db”失败")
except Exception as e:
# exc_info=True 包含错误信息
logger.error(e, exc_info=True)

stdout 输出结果

1
2
3
{"name": "example", "filename": "aa.py", "lineno": 33, "process": 8148, "message": "\u670d\u52a1\u5668\u5728\u7aef\u53e38080\u4e0a\u5f00\u59cb\u4fa6\u542c", "\u7b49\u7ea7": "INFO", "\u65e5\u671f": "2023-07-31 14:19:08,683"}
{"name": "example", "filename": "aa.py", "lineno": 34, "process": 8148, "message": "\u9a71\u52a8\u5668'varlog'\u4e0a\u7684\u78c1\u76d8\u7a7a\u95f4\u4e0d\u8db3\u3002\u8003\u8651\u817e\u51fa\u7a7a\u95f4", "\u7b49\u7ea7": "WARNING", "\u65e5\u671f": "2023-07-31 14:19:08,683"}
{"name": "example", "filename": "aa.py", "lineno": 40, "process": 8148, "message": "\u8fde\u63a5\u6570\u636e\u5e93\u201cmy_db\u201d\u5931\u8d25", "exc_info": "Traceback (most recent call last):\n File \"D:\\project\\knowledge-api\\aa.py\", line 37, in <module>\n raise Exception(\"\u8fde\u63a5\u6570\u636e\u5e93\u201cmy_db\u201d\u5931\u8d25\")\nException: \u8fde\u63a5\u6570\u636e\u5e93\u201cmy_db\u201d\u5931\u8d25", "\u7b49\u7ea7": "ERROR", "\u65e5\u671f": "2023-07-31 14:19:08,683"}

日志文件输出结果

1
2
3
4
5
6
7
8
9
10
{
"name": "example",
"filename": "aa.py",
"lineno": 40,
"process": 8148,
"message": "连接数据库“my_db”失败",
"exc_info": "Traceback (most recent call last):\n File \"D:\\project\\knowledge-api\\aa.py\", line 37, in <module>\n raise Exception(\"连接数据库“my_db”失败\")\nException: 连接数据库“my_db”失败",
"等级": "ERROR",
"日期": "2023-07-31 14:19:08,683"
}

还可以添加额外的附属信息, 例如:

1
2
3
4
logger.info(
"服务器在端口8080上开始侦听",
extra={"python_version": 3.10, "os": "linux", "host": "fedora 38"},
)

Loguru

1
pip install loguru
1
2
3
4
5
6
7
8
9
from loguru import logger

logger.trace("Executing program")
logger.debug("Processing data...")
logger.info("Server started successfully.")
logger.success("Data processing completed successfully.")
logger.warning("Invalid configuration detected.")
logger.error("Failed to connect to the database.")
logger.critical("Unexpected system error occurred. Shutting down.")

日志等级和结构化输出

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from loguru import logger
import sys

# 移除默认配置
logger.remove(0)
# 将日志设置为标准输出,并将日志等级设置为 info,并且设置输出格式为 json
logger.add(sys.stdout, level="INFO", serialize=True)

logger.trace("Executing program")
logger.debug("Processing data...")
logger.info("Server started successfully.")
logger.success("Data processing completed successfully.")
logger.warning("Invalid configuration detected.")
logger.error("Failed to connect to the database.")
logger.critical("Unexpected system error occurred. Shutting down.")

这里虽然设置了 json 输出,但是会包含很多无关的冗余信息。可以自定义消息格式,并且在输出时候添加上下文信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
from loguru import logger
import sys
import json


def serialize(record):
subset = {
"timestamp": record["time"].timestamp(),
"message": record["message"],
"level": record["level"].name,
"file": record["file"].name,
"context": record["extra"],
}
return json.dumps(subset)


def patching(record):
record["extra"]["serialized"] = serialize(record)


logger.remove(0)

logger = logger.patch(patching)
# 将日志输出到标准错误输出流,并且将日志消息的格式指定为 "{extra[serialized]}"
logger.add(sys.stderr, format="{extra[serialized]}")

# 普通使用
logger.info("Processing data...")
# 使用 bind 添加上下文信息
logger.bind(user_id="USR-1243", doc_id="DOC-2348").debug("Processing document")

携带上下文信息

上面代码中使用了 bind 函数来添加上下文信息到日志中,还可以使用它来创建子记录器来记录共享相同上下文的记录:

1
2
3
4
5
# 使用 bind 添加上下文信息
user_log = logger.bind(user_id="USR-1243", doc_id="DOC-2348")
user_log.info("添加用户")
user_log.debug("检查用户是否重复")
user_log.success("添加用户成功")

可以看到以下日志中,都添加了上下文信息。

还可以使用 contextualize 方法来为所有日志都添加上公共的字段,例如,下面的代码片段演示了向由于该请求而创建的所有日志添加唯一的请求 ID 属性:

1
2
3
4
5
6
7
8
9
10
11
12
13
from loguru import logger
import uuid

def logging_middleware(get_response):
def middleware(request):
request_id = str(uuid.uuid4())

with logger.contextualize(request_id=request_id):
response = get_response(request)
response["X-Request-ID"] = request_id
return response

return middleware

Loguru 还为来自标准模块的用户提供了迁移指南

参考:https://betterstack.com/community/guides/logging/best-python-logging-libraries/